/*
 * Copyright 2001-2003 Neil Rotstan Copyright (C) 2004 Derek James and Philip Tucker
 * 
 * This file is part of JGAP.
 * 
 * JGAP is free software; you can redistribute it and/or modify it under the terms of the GNU
 * Lesser Public License as published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 * 
 * JGAP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with JGAP; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 * 02111-1307 USA
 * 
 * Modified on Feb 3, 2003 by Philip Tucker
 */
package org.jgap;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;

import com.anji.neat.ConnectionAllele;
import com.anji.neat.NeuronAllele;

/**
 * Chromosomes represent potential solutions and consist of a fixed-length collection of genes.
 * Each gene represents a discrete part of the solution. Each gene in the Chromosome may be
 * backed by a different concrete implementation of the Gene interface, but all genes with the
 * same innovation ID must share the same concrete implementation across Chromosomes within a
 * single population (genotype).
 */
public class Chromosome implements Comparable<Chromosome>, Serializable {

	/**
	 * default ID
	 */
	public final static Long DEFAULT_ID = new Long( -1 );
	
	/**
	 * 
	 */
	private static final long serialVersionUID = -606608393649690225L;
	
	private SortedSet<Allele> m_alleles = null;
	
	/**
	 * Stores the fitness value of this Chromosome as determined by the active fitness function. A
	 * value of -1 indicates that this field has not yet been set with this Chromosome's fitness
	 * values (valid fitness values are always positive).
	 */
	protected int m_fitnessValue = -1;
	
	private Long m_id = DEFAULT_ID;
	
	private String m_idString;
	
	/**
	 * Keeps track of whether or not this Chromosome has been selected by the natural selector to
	 * move on to the next generation.
	 */
	protected boolean m_isSelectedForNextGeneration = false;
	
	/**
	 * Genetic material contained in this chromosome.
	 */
	private ChromosomeMaterial m_material = null;
	
	/**
	 * Stores the miscellaneous value of this Chromosome as determined by the bulk fitness function.
	 * A value of -1 indicates that this field has not yet been set with this Chromosmome's fitness
	 * values (valid fitness values are always positive).
	 */
//	protected int m_miscValue = -1;
	
	protected Specie m_specie = null;
	
	/**
	 * this should only be called when a chromosome is being created from persistence; otherwise,
	 * the ID should be generated by <code>a_activeConfiguration</code>.
	 * 
	 * @param a_material Genetic material to be contained within this Chromosome instance.
	 * @param an_id unique ID of new chromosome
	 */
	public Chromosome( ChromosomeMaterial a_material, Long an_id ) {
		// Sanity checks: make sure the parameters are all valid.
		if ( a_material == null )
			throw new IllegalArgumentException( "Chromosome material can't be null." );
	
		setId( an_id );
		m_material = a_material;
		m_alleles = Collections.unmodifiableSortedSet( m_material.getAlleles() );
		associateAllelesWithChromosome();
	}
	
	private void associateAllelesWithChromosome() {
		for (Allele allele : m_alleles)
			allele.setChromosome( this );
	}
	
	/**
	 * @return clone with primary parent ID of this chromosome and the same genetic material.
	 */
	public ChromosomeMaterial cloneMaterial() {
		return m_material.clone( getId() );
	}
	
	/**
	 * Compares the given Chromosome to this Chromosome. This chromosome is considered to be "less
	 * than" the given chromosome if it has a fewer number of genes or if any of its gene values
	 * (alleles) are less than their corresponding gene values in the other chromosome.
	 * 
	 * @param o The Chromosome against which to compare this chromosome.
	 * @return a negative number if this chromosome is "less than" the given chromosome, zero if
	 * they are equal to each other, and a positive number if this chromosome is "greater than" the
	 * given chromosome.
	 */
	public int compareTo( Chromosome other ) {
		return m_id.compareTo( other.m_id );
	}
	
	public int countLinks() {
		return getConnections().size();
	}
	
	public int countNodes() {
		return getNodes().size();
	}
	
	/**
	 * Calculates compatibility distance between this and <code>target</code> according to <a
	 * href="http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf">NEAT </a> speciation
	 * methodology. It is generic enough that the alleles do not have to be nodes and connections.
	 * 
	 * @param target
	 * @param parms
	 * @return distance between this object and <code>target</code>
	 * @see ChromosomeMaterial#distance(ChromosomeMaterial, SpeciationParms)
	 */
	public double distance( Chromosome target, SpeciationParms parms ) {
		return m_material.distance( target.m_material, parms );
	}
	
	/**
	 * Compares this Chromosome against the specified object. The result is true if and the argument
	 * is an instance of the Chromosome class and has a set of genes equal to this one.
	 * 
	 * @param other The object to compare against.
	 * @return true if the objects are the same, false otherwise.
	 */
	public boolean equals( Chromosome other ) {
		return compareTo( other ) == 0;
	}
	
	/**
	 * @param alleleToMatch
	 * @return Gene gene with same innovation ID as
	 * <code>geneToMatch</code, or <code>null</code> if none match
	 */
	public Allele findMatchingGene( Allele alleleToMatch ) {
		for (Allele allele : m_alleles) {
			if ( allele.equals( alleleToMatch ) )
				return allele;
		}
		return null;
	}
	
	/**
	 * @return SortedSet alleles, sorted by innovation ID
	 */
	public SortedSet<Allele> getAlleles() {
		return m_alleles;
	}
	
	public List<ConnectionAllele> getConnections() {
		List<ConnectionAllele> connections = new ArrayList<ConnectionAllele>();
		for (Allele a : m_alleles)
			if (a instanceof ConnectionAllele) connections.add( (ConnectionAllele) a );
		return connections;
	}
	
	/**
	 * Retrieves the fitness value of this Chromosome, as determined by the active fitness function.
	 * @return a positive integer value representing the fitness of this Chromosome, or -1 if
	 * fitness function has not yet assigned a fitness value to this Chromosome.
	 */
	public int getFitnessValue() {
		return m_fitnessValue;
	}
	
	/**
	 * @return Long unique identifier for chromosome; useful for <code>hashCode()</code> and
	 * persistence
	 */
	public Long getId() {
		return m_id;
	}
	
	/**
	 * Retrieves the miscellaneous value of this Chromosome, as determined by the bulk fitness function.
	 * @return a positive integer value representing the fitness of this Chromosome, or -1 if
	 * fitness function has not yet assigned a fitness value to this Chromosome.
	 */
//	public int getMiscValue() {
//		return m_miscValue;
//	}
	
	public List<NeuronAllele> getNodes() {
		List<NeuronAllele> nodes = new ArrayList<NeuronAllele>();
		for (Allele a : m_alleles)
			if (a instanceof NeuronAllele) nodes.add( (NeuronAllele) a);
		return nodes;
	}
	
	/**
	 * @return primary parent ID; this is the dominant parent for chromosomes spawned by crossover,
	 * and the only parent for chromosomes spawned by cloning
	 */
	public Long getPrimaryParentId() {
		return m_material.getPrimaryParentId();
	}
	
	/**
	 * @return secondary parent ID; this is the recessive parent for chromosomes spawned by
	 * crossover, and null for chromosomes spawned by cloning
	 */
	public Long getSecondaryParentId() {
		return m_material.getSecondaryParentId();
	}
	
	/**
	 * @return int fitness value adjusted for fitness sharing according to <a
	 * href="http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf>NEAT </a> paradigm.
	 */
	public int getSpeciatedFitnessValue() {
		if ( m_specie == null )
			return getFitnessValue();
		int result = (int) Math.round(m_specie.getChromosomeFitnessValue( this ));
		return Math.max(1, result);
	}

	/**
	 * @return this chromosome's specie
	 */
	public Specie getSpecie() {
		return m_specie;
	}
	
	/**
	 * Retrieve a hash code for this Chromosome.
	 * 
	 * @return the hash code of this Chromosome.
	 */
	public int hashCode() {
		return m_id.hashCode();
	}
	
	/**
	 * Retrieves whether this Chromosome has been selected by the natural selector to continue to
	 * the next generation.
	 * 
	 * @return true if this Chromosome has been selected, false otherwise.
	 */
	public boolean isSelectedForNextGeneration() {
		return m_isSelectedForNextGeneration;
	}
	
	/**
	 * Sets the fitness value of this Chromosome. This method is for use by bulk fitness functions
	 * and should not be invoked from anything else. This is the raw fitness value, before species
	 * fitness sharing.
	 * 
	 * @param a_newFitnessValue a positive integer representing the fitness of this Chromosome. if
	 * 0, fitness is set as 1.
	 */
	public void setFitnessValue( int a_newFitnessValue ) {
		if ( a_newFitnessValue > 0 )
			m_fitnessValue = a_newFitnessValue;
		else
			m_fitnessValue = 1;
	}
	
	/**
	 * for hibernate
	 * @param id
	 */
	private void setId( Long id ) {
		m_id = id;
		m_idString = "Chromosome " + m_id;
	}
	
	/**
	 * Sets whether this Chromosome has been selected by the natural selector to continue to the
	 * next generation.
	 * 
	 * @param a_isSelected true if this Chromosome has been selected, false otherwise.
	 */
	public void setIsSelectedForNextGeneration( boolean a_isSelected ) {
		m_isSelectedForNextGeneration = a_isSelected;
	}
	
	/**
	 * Sets a miscellaneous value for this Chromosome.  This method is used to record alternate
	 * fitness information during evolution, e.g. during novelty search (which uses sparseness as the
	 * primary fitness value), it is still important to record the traditional fitness even if it does
	 * not affect selection.  This info is written to the run log to be used later.
	 * @param i
	 */
//	public void setMiscValue( int a_newMiscValue ) {
//		if ( a_newMiscValue > 0 )
//			m_miscValue = a_newMiscValue;
//		else
//			m_miscValue = 1;		
//	}
	
	/**
	 * should only be called from Specie; assigns this chromosome to <code>aSpecie</code>; throws
	 * exception if chromosome is added to a specie twice
	 * 
	 * @param aSpecie
	 */
	void setSpecie( Specie aSpecie ) {
		if ( m_specie != null )
			throw new IllegalStateException( "chromosome can't be added to " + aSpecie
					+ ", already a member of specie " + m_specie );
		m_specie = aSpecie;
	}
	
	/**
	 * Returns the size of this Chromosome (the number of alleles it contains). A Chromosome's size
	 * is constant and will never change.
	 * 
	 * @return The number of alleles contained within this Chromosome instance.
	 */
	public int size() {
		return m_alleles.size();
	}
	
	/**
	 * Returns a string representation of this Chromosome, useful for some display purposes.
	 * 
	 * @return A string representation of this Chromosome.
	 */
	public String toString() {
		return m_idString;
	}
}
